package com.cutler.kelv.wordduel;

import java.io.IOException;
import java.util.Scanner;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONException;
import org.json.JSONObject;

import com.googlecode.objectify.Key;
import com.googlecode.objectify.NotFoundException;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;
import com.googlecode.objectify.Query;

@SuppressWarnings("serial")
public class WordDuelServlet extends HttpServlet {

	private final static String CREATE_USER = "createUser";
	private final static String SIGN_IN = "signIn";
	private final static String START_GAME_WITH = "startGameWith";
	private final static String GET_GAME_WITH = "getGameWith";
	private final static String GET_GAMES_WITH = "getGameWith";
	private final static String GET_THEIR_LAST_GUESS = "getTheirLastGuess";
	private final static String ENTER_SECRET_WORD = "enterSecretWord";
	private final static String GUESS_WORD = "guessWord";
	private final static String CROSS_LETTERS = "crossLetters";
	private final static String CIRCLE_LETTERS = "circleLetters";
	private final static String UNMARK_LETTERS = "unmarkLetters";
	private final static String CHECK_WORD = "checkWord";
	private final static String LIST_CURRENT_GAMES = "listCurrentGames";
	private final static String POPULATE_WORDS = "populateWords";
	
	static {
		ObjectifyService.register(UserInfo.class);
		ObjectifyService.register(Game.class);
		ObjectifyService.register(ValidWord.class);
	}
	
	public WordDuelServlet() {
		super();
	}
	
	private void processRequest(JSONObject params, HttpServletResponse resp) throws IOException, JSONException {
		if (!params.has("action")) {
			resp.setContentType("text/plain");
			resp.getWriter().println("Missing Action");
			return;
		}
		resp.setContentType("application/json");
		
		Objectify ofy = ObjectifyService.begin();
		
		String action = params.getString("action");
		
		if (action.equals(POPULATE_WORDS)) {
			for (String word: validWords) {
				ValidWord validWord = new ValidWord(word, 1);
				ofy.put(validWord);
			}
			
			resp.getWriter().println("Success!");
			return;
		}
		 

		if (action.equals(CHECK_WORD)) {
			if (!params.has("word")) {
				error(resp, "Missing Parameter");
				return;
			}
			resp.getWriter().println("{\"valid\":"+getWordIsValid(ofy, params.getString("word"))+"}");
			return;
		}
		else if (action.equals(CREATE_USER)) {
			if (!params.has("passwordHash") || !params.has("myEmail")
					 || !params.has("name")) {
				error(resp, "Missing Parameters");
				return;
			}
			UserInfo userInfo = null;
			try {
				userInfo = ofy.get(UserInfo.class, params.getString("myEmail"));
			}
			catch (NotFoundException e) {}
			if (userInfo != null && userInfo.passwordHash != null) {
				error(resp, "User Already Exists");
				return;
			}
			
			if (userInfo == null) {
				userInfo = new UserInfo();
			}
			userInfo.email = params.getString("myEmail");
			userInfo.name = params.getString("name");
			userInfo.passwordHash = params.getString("passwordHash");
			userInfo.pin = (int)(Math.random()*Integer.MAX_VALUE);
			ofy.put(userInfo);
			
			resp.getWriter().println(userInfo.toJSON(true).toString());
			return;
		}
		else if (action.equals(SIGN_IN)) {
			if (!params.has("passwordHash") || !params.has("myEmail")) {
				error(resp, "Missing Parameters");
				return;
			}
			UserInfo userInfo = null;
			
			try {
				userInfo = ofy.get(UserInfo.class, params.getString("myEmail"));
			}
			catch (NotFoundException e) {}
			
			if (userInfo == null) {
				error(resp, "Unknown User");
				return;
			}
			if (userInfo.passwordHash == null || !userInfo.passwordHash.equals(params.getString("passwordHash"))) {
				error(resp, "Incorrect Password");
				return;
			}
			
			userInfo.pin = (int)(Math.random()*Integer.MAX_VALUE);
			ofy.put(userInfo);
			
			resp.getWriter().println(userInfo.toJSON(true).toString());
			return;
		}

		if (!params.has("pin") || !params.has("myEmail")) {
			error(resp, "Missing User Identifying Parameter");
			return;
		}
		UserInfo myInfo = null;
		try {
			myInfo = ofy.get(UserInfo.class, params.getString("myEmail"));
		}
		catch (NotFoundException e) {}
		if (myInfo == null || myInfo.pin != params.optInt("pin")) {
			error(resp, true);
			return;
		}
		
		if (action.equals(START_GAME_WITH)) {
			if (!params.has("theirEmail")) {
				error(resp, "Missing Parameter(s)");
				return;
			}
			UserInfo theirInfo = null;
			
			try {
				theirInfo = ofy.get(UserInfo.class, params.getString("theirEmail"));
			}
			catch (NotFoundException e) {
				theirInfo = new UserInfo();
				theirInfo.email = params.getString("theirEmail");
				ofy.put(theirInfo);
			}
			
			String emailCombo = combineEmails(myInfo.email, theirInfo.email);
			Game game = ofy.query(Game.class).filter("playerIds", emailCombo).filter("active", true).get();
			
			if (game != null) {
				game.active = false;
				ofy.put(game);
			}
			game = new Game(myInfo, theirInfo, emailCombo);

			ofy.put(game);
			
			resp.getWriter().println(game.toString());
			return;
		}
		else if (action.equals(GET_GAME_WITH)) {
			if (!params.has("theirEmail")) {
				error(resp, "Missing Parameter(s)");
				return;
			}
			UserInfo theirInfo = null;
			
			try {
				theirInfo = ofy.get(UserInfo.class, params.getString("theirEmail"));
			}
			catch (NotFoundException e) {
				theirInfo = new UserInfo();
				theirInfo.email = params.getString("theirEmail");
				ofy.put(theirInfo);
			}
			
			String emailCombo = combineEmails(myInfo.email, theirInfo.email);
			Game game = null;
			try {
				game = ofy.query(Game.class).filter("playerIds", emailCombo).filter("active", true).get();
			}
			catch (NotFoundException e) {
				error(resp, "No game with "+theirInfo.email);
				return;
			}
			game.load(ofy);

			resp.getWriter().println(game.toString());
			return;
		}
		else if (action.equals(GET_GAMES_WITH)) {
			if (!params.has("theirEmail")) {
				error(resp, "Missing Parameter(s)");
				return;
			}
			UserInfo theirInfo = null;
			
			try {
				theirInfo = ofy.get(UserInfo.class, params.getString("theirEmail"));
			}
			catch (NotFoundException e) {
				theirInfo = new UserInfo();
				theirInfo.email = params.getString("theirEmail");
				ofy.put(theirInfo);
			}
			
			String emailCombo = combineEmails(myInfo.email, theirInfo.email);
			
			JSONObject gameList = new JSONObject();

			try {
				Query<Game> query = ofy.query(Game.class).filter("playerIds", emailCombo);
				for (Game game: query.fetch()) {
					game.load(ofy);
					gameList.append("Games", game.toJSON());
				}
			} catch (JSONException e) {}
			
			resp.getWriter().println(gameList.toString());
			return;
		}
		else if (action.equals(GET_THEIR_LAST_GUESS)) {
			if (!params.has("gameId") || !params.has("index")) {
				error(resp, "Missing Parameter(s)");
				return;
			}

			Game game = getGame(ofy, params.optLong("gameId"));

			if (game == null) {
				error(resp, "Invalid Parameter(s)");
				return;
			}
			
			JSONObject result = new JSONObject();
			
			int index = params.optInt("index");
			try {
				if (myInfo.email.equals(game.p1Email)) {
					if (game.p2Guesses == null || index >= game.p2Guesses.length) {
						result.put("exists",false);
					}
					else {
						result.put("exists", true);
						result.put("word", game.p2Guesses[index]);
						result.put("guessCount", Game.getGuessCount(game.p1Word, game.p2Guesses[index]));
					}
				}
				else {
					if (game.p1Guesses == null || index >= game.p1Guesses.length) {
						result.put("exists",false);
					}
					else {
						result.put("exists", true);
						result.put("word", game.p1Guesses[index]);
						result.put("guessCount", Game.getGuessCount(game.p2Word, game.p1Guesses[index]));
					}
				}
			} catch (JSONException e) {}

			resp.getWriter().println(result.toString());
			return;
		} else if (action.equals(ENTER_SECRET_WORD)) {
			if (!params.has("word") || !params.has("gameId")) {
				error(resp, "Missing Parameter(s)");
				return;
			}
			String secretWord = params.getString("word").toLowerCase();
			if (secretWord.length() != 4) {
				error(resp, "Invalid word - secret word must be four letters long.");
				return;
			}
			if (!getWordIsValid(ofy, secretWord)) {
				error(resp, "Invalid word - not found in game dictionary.");
				return;
			}

			Game game = getGame(ofy, params.optLong("gameId"));
			
			if (game == null) {
				error(resp, "Invalid Parameter(s)");
				return;
			}
			
			if (myInfo.email.equals(game.p1Email)) {
				if (game.p2Guesses == null || game.p2Guesses.length == 0) {
					game.p1Word = secretWord;
				} 
				else {
					error(resp, "Can't set secret word after the other player has guessed.");
					return;
				}
			}
			else {
				if (game.p1Guesses == null || game.p1Guesses.length == 0) {
					game.p2Word = secretWord;
				} 
				else {
					error(resp, "Can't set secret word after the other player has guessed.");
					return;
				}
			}
			
			ofy.put(game);

			resp.getWriter().println(game.toString());
			return;
		}
		else if (action.equals(GUESS_WORD)) {
			if (!params.has("word") || !params.has("gameId")) {
				error(resp, "Missing Parameter(s)");
				return;
			}
			String guessWord = params.getString("word").toLowerCase();
			if (guessWord.length() != 4) {
				error(resp, "Invalid word - guess must be four letters long.");
				return;
			}
			if (!getWordIsValid(ofy, guessWord)) {
				error(resp, "Invalid word - not found in game dictionary.");
				return;
			}

			Game game = getGame(ofy, params.optLong("gameId"));
			
			if (game == null) {
				error(resp, "Invalid Parameter(s)");
				return;
			}
			if (!game.active) {
				error(resp, "Game has ended.");
				return;
			}

			String[] guesses;
			if (myInfo.email.equals(game.p1Email)) {
				if (game.p1Guesses == null) {
					guesses = new String[] { guessWord };
				}
				else {
					if (game.p1Guesses.length > game.p2Guesses.length) {
						error(resp, "Must wait for other player to guess");
						return;
					}
					guesses = new String[game.p1Guesses.length+1];
					for (int i=0; i<game.p1Guesses.length; i++)
						guesses[i] = game.p1Guesses[i];
					guesses[game.p1Guesses.length] = guessWord;
				}
				game.p1Guesses = guesses;
			}
			else {
				if (game.p2Guesses == null) {
					guesses = new String[] { guessWord };
				}
				else {
					if (game.p2Guesses.length > game.p1Guesses.length) {
						error(resp, "Must wait for other player to guess");
						return;
					}
					guesses = new String[game.p2Guesses.length+1];
					for (int i=0; i<game.p2Guesses.length; i++)
						guesses[i] = game.p2Guesses[i];
					guesses[game.p2Guesses.length] = guessWord;
				}
				game.p2Guesses = guesses;
			}
			if (game.getWinner() != null) {
				game.active = false;
			}
			
			ofy.put(game);

			resp.getWriter().println(game.toString());
			return;
		}
		else if (action.equals(CROSS_LETTERS)) {
			if (!params.has("letters") || !params.has("gameId")) {
				error(resp, "Missing Parameter(s)");
				return;
			}

			Game game = getGame(ofy, params.optLong("gameId"));
			
			if (game == null) {
				error(resp, "Invalid Parameter(s)");
				return;
			}
			if (!game.active) {
				error(resp, "Game has ended.");
				return;
			}

			String letters = params.getString("letters").toLowerCase();
			
			for (int i=0; i<letters.length(); i++) {
				if (alphabet.indexOf(letters.charAt(i)) < 0) {
					error(resp, "Letters must be a-z");
					return;
				}
			}
			
			if (myInfo.email.equals(game.p1Email)) {
				for (int i=0; i<letters.length(); i++) {
					int letterIndex = letters.charAt(i) - 'a';
					game.p1Alphabet = game.p1Alphabet.substring(0, letterIndex) + '_' + game.p1Alphabet.substring(letterIndex+1);
				}
			}
			else {
				for (int i=0; i<letters.length(); i++) {
					int letterIndex = letters.charAt(i) - 'a';
					game.p2Alphabet = game.p2Alphabet.substring(0, letterIndex) + '_' + game.p2Alphabet.substring(letterIndex+1);
				}
			}
			
			ofy.put(game);

			resp.getWriter().println(game.toString());
			return;
		}
		else if (action.equals(CIRCLE_LETTERS)) {
			if (!params.has("letters") || !params.has("gameId")) {
				error(resp, "Missing Parameter(s)");
				return;
			}
			
			Game game = getGame(ofy, params.optLong("gameId"));
			
			if (game == null) {
				error(resp, "Invalid Parameter(s)");
				return;
			}
			if (!game.active) {
				error(resp, "Game has ended.");
				return;
			}
			
			String letters = params.getString("letters").toLowerCase();
			
			for (int i=0; i<letters.length(); i++) {
				if (alphabet.indexOf(letters.charAt(i)) < 0) {
					error(resp, "Letters must be a-z");
					return;
				}
			}
			
			if (myInfo.email.equals(game.p1Email)) {
				for (int i=0; i<letters.length(); i++) {
					int letterIndex = letters.charAt(i) - 'a';
					game.p1Alphabet = game.p1Alphabet.substring(0, letterIndex) + (char)('A'+letterIndex) + game.p1Alphabet.substring(letterIndex+1);
				}
			}
			else {
				for (int i=0; i<letters.length(); i++) {
					int letterIndex = letters.charAt(i) - 'a';
					game.p2Alphabet = game.p2Alphabet.substring(0, letterIndex) + (char)('A'+letterIndex) + game.p2Alphabet.substring(letterIndex+1);
				}
			}
			
			ofy.put(game);

			resp.getWriter().println(game.toString());
			return;
		}
		else if (action.equals(UNMARK_LETTERS)) {
			if (!params.has("letters") || !params.has("gameId")) {
				error(resp, "Missing Parameter(s)");
				return;
			}
			
			Game game = getGame(ofy, params.optLong("gameId"));
			
			if (game == null) {
				error(resp, "Invalid Parameter(s)");
				return;
			}
			if (!game.active) {
				error(resp, "Game has ended.");
				return;
			}
			
			String letters = params.getString("letters").toLowerCase();
			
			for (int i=0; i<letters.length(); i++) {
				if (alphabet.indexOf(letters.charAt(i)) < 0) {
					error(resp, "Letters must be a-z");
					return;
				}
			}
			
			if (myInfo.email.equals(game.p1Email)) {
				for (int i=0; i<letters.length(); i++) {
					int letterIndex = letters.charAt(i) - 'a';
					game.p1Alphabet = game.p1Alphabet.substring(0, letterIndex) + (char)('a'+letterIndex) + game.p1Alphabet.substring(letterIndex+1);
				}
			}
			else {
				for (int i=0; i<letters.length(); i++) {
					int letterIndex = letters.charAt(i) - 'a';
					game.p2Alphabet = game.p2Alphabet.substring(0, letterIndex) + (char)('a'+letterIndex) + game.p2Alphabet.substring(letterIndex+1);
				}
			}
			
			ofy.put(game);

			resp.getWriter().println(game.toString());
			return;
		}
		else if (action.equals(LIST_CURRENT_GAMES)) {
			JSONObject gameList = new JSONObject();

			try {
				Query<Game> query = ofy.query(Game.class).filter("p1Email", myInfo.email).filter("active", true);
				for (Game game: query.fetch()) {
					game.load(ofy);
					gameList.append("Mine", game.toJSON());
				}
				
				query = ofy.query(Game.class).filter("p2Email", myInfo.email).filter("active", true);
				for (Game game: query.fetch()) {
					game.load(ofy);
					gameList.append("Theirs", game.toJSON());
				}
			} catch (JSONException e) {}
			
			resp.getWriter().println(gameList.toString());
			return;
		}
	}
	
	@Override
	protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		resp.setHeader("Access-Control-Allow-Origin", "*");
		resp.setHeader("Access-Control-Allow-Methods", "POST, GET");
		String postData = "";
		try {
	        postData = new Scanner(req.getInputStream()).useDelimiter("\\A").next();
	    } catch (java.util.NoSuchElementException e) {
			resp.getWriter().println("{\"error\":\""+e.toString()+"\"}");
		}
		
		JSONObject jObject = null;
		try {
			jObject = new JSONObject(postData);
		} catch (JSONException e) {
			resp.getWriter().println("{\"error\":\""+e.toString()+"\"}");
		}
		
		if (jObject == null) {
			resp.setContentType("text/plain");
			resp.getWriter().println("{\"error\":\"Invalid JSON\"}");
			return;
		}
		
		try {
			processRequest(jObject, resp);
		} catch (JSONException e) {
			resp.getWriter().println("{\"error\":\""+e.toString()+"\"}");
		}
	}
	
	public void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
		resp.setHeader("Access-Control-Allow-Origin", "*");
		resp.setHeader("Access-Control-Allow-Methods", "POST, GET");
		JSONObject jObject = new JSONObject();

		try {
			for (String paramName: expectedParams) {
				if (req.getParameterMap().containsKey(paramName))
					jObject.put(paramName, req.getParameter(paramName));
			}
			
			processRequest(jObject, resp);
		} catch (JSONException e) {}
	}
	
	private Game getGame(Objectify ofy, long gameId) {
		Game game = null;
		try {
			game = ofy.get(new Key<Game>(Game.class, gameId));
		}
		catch (NotFoundException e) {
			return null;
		}
		
		game.load(ofy);

		return game;
	}
	
	private boolean getWordIsValid(Objectify ofy, String word) {
		return getWordIsValid(ofy, word, 1);
	}
	
	private boolean getWordIsValid(Objectify ofy, String word, int commonRank) {
		try {
			return ofy.get(new Key<ValidWord>(ValidWord.class, word.toLowerCase())) != null;
		} catch (NotFoundException e) {
			return false;
		}
	}

	
	private void error(HttpServletResponse resp, boolean needToSignIn) {
		try {
			resp.getWriter().println("{\"signIn\":true}");
		} catch (IOException e) {}
	}
	
	private void error(HttpServletResponse resp, String errorMessage) {
		try {
			resp.getWriter().println("{\"error\":\""+errorMessage+"\"}");
		} catch (IOException e) {}
	}
	
	private String emailCharactersArray = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.!#$%&'*+-/=?^_`{|}~\"(),:;<>@[]\\";
	
	private String combineEmails(String email1, String email2) {
		String combination = "";
		for (int i=0; i<Math.max(email1.length(), email2.length()); i++) {
			if (i >= email1.length()) {
				combination += (char)(emailCharactersArray.indexOf(email2.charAt(i)));
			} else if (i >= email2.length()) {
				combination += (char)(emailCharactersArray.indexOf(email1.charAt(i)));
			} else {
				combination += (char)(emailCharactersArray.indexOf(email1.charAt(i))+emailCharactersArray.indexOf(email2.charAt(i)));
			}
		}
		return combination;
	}
	
	
	private String alphabet = "abcdefghijklmnopqrstuvwxyz";
	private String[] expectedParams = new String[] {"action", "name", 
			"myEmail", "passwordHash", "pin","gameId", "theirEmail",
			"word","letters","index"};
	
	
	
	private final static String[] validWords = new String[] {
		"abet","able","ably","abut","aced","aces","ache","achy","acid","acme","acne","acre","acts","adds","adze","aeon","aero","afar","agar","aged",
		"ager","ages","ague","ahem","ahoy","aide","aids","ails","aims","airs","airy","ajar","akin","alas","alba","alfa","alit","alls","ally","alma",
		"alms","aloe","alps","also","alto","alum","amen","amid","ammo","amok","amps","anal","anew","ankh","anon","ante","anti","ants","anus","apes",
		"apex","apse","aqua","arch","arcs","area","ares","arfs","aria","arid","arks","arms","army","arse","arts","arty","ashy","asks","asps","atom",
		"atop","aunt","aura","auto","aver","avid","avow","away","awed","awes","awls","awry","axed","axel","axes","axis","axle","axon","ayes","baal",
		"baas","baba","babe","baby","bach","back","bade","bads","bags","bail","bait","bake","bald","bale","balk","ball","balm","bams","band","bane",
		"bang","bank","bans","barb","bard","bare","barf","bark","barn","bars","base","bash","bask","bass","bate","bath","bats","batt","baud","bawl",
		"bays","bead","beak","beam","bean","bear","beat","beau","beds","beef","been","beep","beer","bees","beet","begs","bell","belt","bend","bent",
		"berg","best","beta","bets","bevy","bias","bibs","bide","bids","bier","biff","bigs","bike","bile","bill","bind","bins","bios","bird","bite",
		"bits","blab","blah","blam","blat","bled","blew","blip","blob","blot","blow","blub","blue","blur","boar","boas","boat","bobs","bode","body",
		"bogs","bogy","boil","bold","bolo","bolt","bomb","bond","bone","bong","bonk","bony","boob","book","boom","boon","boos","boot","bops","bore",
		"born","boss","both","bots","bout","bowl","bows","boxy","boys","bozo","brad","brae","brag","bran","bras","brat","bray","bred","bree","brew",
		"brie","brig","brim","brit","bros","brow","bubs","buck","buds","buff","bugs","bulb","bulk","bull","bump","bums","bunk","buns","bunt","buoy",
		"burg","burn","burp","burr","burs","bury","bush","buss","bust","busy","bute","buts","butt","buys","buzz","byes","byte","cabs","cads","cafe",
		"cage","cake","caky","calf","calk","call","calm","came","camp","cams","cane","cans","cant","cape","caps","carb","card","care","carp","cars",
		"cart","case","cash","cask","cast","cats","caul","cave","caws","cede","cell","cent","chad","chap","chat","chaw","chef","chew","chic","chin",
		"chip","chop","chow","chub","chug","chum","ciao","cite","city","clad","clam","clan","clap","claw","clay","clef","clip","clod","clog","clop",
		"clot","club","clue","coal","coat","coax","cobb","cobs","cock","coda","code","coed","cogs","coil","coin","coke","cola","cold","colt","coma",
		"comb","come","cone","cons","cook","cool","coon","coop","coos","coot","cope","cops","copy","cord","core","cork","corn","cost","cots","coup",
		"cove","cowl","cows","cowy","coxa","cozy","crab","crag","cram","crap","craw","crew","crib","crop","crow","crud","crux","cube","cubs","cuds",
		"cued","cues","cuff","cull","cult","cups","curb","curd","cure","curl","curs","curt","cusp","cuss","cute","cuts","cyan","cyst","czar","dads",
		"daft","dais","dale","dame","damp","dams","dang","dank","dare","dark","darn","dart","dash","data","date","daub","dawn","days","daze","dead",
		"deaf","deal","dean","dear","debt","deck","deco","deed","deem","deep","deer","deft","defy","deli","dell","demo","dens","dent","deny","derm",
		"desk","devs","dews","dial","dibs","dice","dido","died","dies","diet","digs","dike","dill","dime","dims","dine","ding","dins","dips","dire",
		"dirk","dirt","disc","dish","disk","diss","ditz","diva","dive","djin","dock","docs","dodo","doer","does","doge","dogs","dogy","dojo","dole",
		"doll","dolt","dome","done","dong","dons","doom","door","dope","dork","dorm","dost","dote","doth","dots","dour","dove","down","dows","doze",
		"dozy","drab","drag","dram","drat","draw","dreg","drip","drop","drug","drum","drys","dual","dubs","duce","duck","duct","dude","duds","duel",
		"dues","duet","duke","dull","duly","dumb","dump","dune","dung","dunk","duos","dupe","dusk","dust","duty","dyed","dyer","dyes","dyke","dyne",
		"each","earl","earn","ears","ease","east","easy","eats","eave","ebbs","ebon","echo","eddy","edge","edgy","edit","eels","eely","eery","egad",
		"eggs","eggy","egos","eked","ekes","elks","ells","elms","elmy","else","emit","emus","ends","envy","eons","epic","eras","ergo","erne","errs",
		"erst","espy","etch","euro","even","ever","eves","evil","ewes","exam","exec","exes","exit","expo","eyed","eyer","eyes","eyry","face","fact",
		"fade","fads","fags","fail","fain","fair","fake","fall","fame","fang","fans","fare","farm","fart","fast","fate","fats","faun","faux","fava",
		"fawn","faze","fear","feat","feck","feds","feed","feel","fees","feet","fell","felt","fend","fens","fern","fess","feud","fibs","fido","fife",
		"figs","file","fill","film","find","fine","fink","fins","fire","firm","firn","firs","fish","fist","fits","five","fizz","flab","flag","flak",
		"flan","flap","flat","flaw","flax","flay","flea","fled","flee","flew","flex","flip","flit","flog","flop","flow","flub","flue","flux","foal",
		"foam","fobs","foci","foes","fogs","fogy","foil","fold","folk","fond","font","food","fool","foot","ford","fore","fork","form","fort","foul",
		"four","fowl","foxy","frag","frap","frat","fray","free","fret","frog","from","fued","fuel","fuji","full","fume","fumy","fund","funk","funs",
		"furl","furs","fury","fuse","fuss","fuze","fuzz","gabs","gads","gaga","gags","gain","gait","gala","gale","gall","gals","game","gams","gamy",
		"gang","gape","gaps","gapy","garb","gash","gasp","gast","gate","gave","gawk","gays","gaze","gear","geek","gees","geez","gels","gems","gene",
		"gent","germ","gets","gift","giga","gigs","gild","gill","gilt","gimp","gins","gird","girl","girt","gist","gits","give","glad","glee","glen",
		"glob","glop","glow","glue","glug","glum","glut","gnat","gnaw","goad","goal","goat","gobs","gods","goer","goes","gold","golf","gone","gong",
		"good","goof","goon","goop","goos","gore","gory","gosh","gout","gown","grab","grad","gram","gray","grew","grey","grid","grim","grin","grip",
		"grit","grog","grow","grub","guar","gulf","gull","gulp","gums","gunk","guns","guru","gush","gust","guts","guys","gyms","gyps","gyro","hack",
		"hags","haha","hail","hair","half","hall","halo","halt","hams","hand","hang","hard","hare","hark","harm","harp","hart","hash","hast","hate",
		"hath","hats","haul","have","hawk","hays","haze","hazy","head","heal","heap","hear","heat","heck","heed","heel","heft","heir","held","helm",
		"help","hemp","hems","hens","herb","herd","here","hero","hers","hewn","hews","hick","hide","hied","hies","high","hike","hill","hilt","hind",
		"hint","hips","hire","hiss","hits","hive","hoar","hoax","hobo","hobs","hock","hoed","hoer","hoes","hogs","hold","hole","holy","home","homo",
		"hone","honk","hood","hoof","hook","hoop","hoot","hope","hops","horn","hose","host","hots","hour","howl","hows","hubs","huck","hues","huff",
		"huge","hugs","hula","hulk","hull","hump","hums","hung","hunk","huns","hunt","hurl","hurt","hush","husk","huts","hymn","hype","iamb","ibex",
		"ibis","iced","ices","icky","icon","idea","ides","idle","idly","idol","iffy","ills","imps","inch","info","inks","inky","inns","into","ions",
		"iota","iris","irks","iron","isle","itch","item","jabs","jack","jade","jags","jail","jams","jane","jars","java","jaws","jays","jazz","jean",
		"jeep","jeer","jerk","jest","jets","jews","jibe","jigs","jill","jimp","jinx","jive","jobs","jock","joes","joey","jogs","john","join","joke",
		"jolt","josh","jots","jowl","joys","judo","jugs","juju","juke","jump","junk","jury","just","juts","kale","kana","kart","kays","keel","keen",
		"keep","kegs","kelp","kept","keys","kick","kids","kill","kiln","kilo","kilt","kind","kine","king","kink","kins","kirk","kiss","kite","kits",
		"kiwi","knap","knee","knew","knit","knob","knot","know","kook","kudo","labs","lace","lack","lacy","lads","lady","lags","laid","lain","lair",
		"lake","lamb","lame","lamp","land","lane","lank","laps","lard","lark","lash","lass","last","late","laud","lava","lawn","laws","lays","lazy",
		"lead","leaf","leak","lean","leap","leek","leer","lees","left","legs","lend","lens","lent","lept","less","lest","lets","levy","lewd","liar",
		"lice","lich","lick","lids","lied","lien","lier","lies","lieu","life","lift","like","lilt","lily","lima","limb","lime","limo","limp","line",
		"link","lint","lion","lips","lisp","list","live","load","loaf","loam","loan","lobe","lobs","loch","lock","lode","loft","logo","logs","loin",
		"lone","long","look","loom","loon","loop","loot","lope","lops","lord","lore","lose","loss","lost","lots","loud","love","lows","lube","luck",
		"luge","lugs","lull","lump","lung","lure","lurk","lush","lust","lute","lyes","lynx","lyre","mace","mach","mack","made","mage","magi","maid",
		"mail","maim","main","make","male","mall","malt","mama","mana","mane","mans","many","maps","mare","mark","mars","mart","mash","mask","mass",
		"mast","mate","math","mats","maul","maws","maya","mayo","mays","maze","mead","meal","mean","meat","meek","meet","meld","melt","meme","memo",
		"mend","menu","meow","mere","mesa","mesh","mess","meta","mete","meth","mews","mhos","mice","midi","miff","mike","mild","mile","milk","mill",
		"mime","mind","mine","mini","mink","mint","minx","mire","miss","mist","mite","mitt","moan","moat","mobs","mock","mode","mods","mojo","mold",
		"mole","moms","monk","mono","mood","moon","moor","moos","moot","mope","mops","more","morn","moss","most","mote","moth","move","mows","much",
		"muck","muds","mugs","mule","mull","mums","mumu","muon","muse","mush","musk","must","mute","mutt","myth","nabs","nags","nail","name","nape",
		"naps","nary","navy","nays","nazi","near","neat","neck","need","neem","neep","neon","nerd","nest","nets","news","newt","next","nice","nick",
		"nigh","nine","nips","nock","node","nods","noel","noes","none","nook","noon","nope","norm","nose","nosy","note","noun","nova","nude","nuke",
		"null","numb","nuns","nuts","oafs","oaks","oars","oath","oats","obey","oboe","odds","odes","odor","offs","ogle","ogre","ohms","oils","oily",
		"oink","okay","okra","omen","omit","once","ones","only","onto","onus","onyx","oohs","oops","ooze","oozy","opal","open","opts","opus","oral",
		"orbs","orca","orcs","ores","orgy","ouch","ours","oust","outs","oval","oven","over","ovum","owed","owes","owls","owns","oxen","pace","pack",
		"pact","pads","page","paid","pail","pain","pair","pale","pall","palm","pals","pane","pang","pans","pant","papa","paps","pare","park","part",
		"pass","past","pate","path","pats","pave","pawn","paws","pays","peak","peal","pear","peas","peat","peck","pecs","peed","peek","peel","peep",
		"peer","pees","pegs","pelt","pens","pent","peon","perk","perm","peso","pest","pets","pews","phew","pick","pied","pier","pies","pigs","pike",
		"pile","pill","pimp","pine","ping","pink","pins","pint","pion","pipe","pips","piss","pita","pith","pits","pity","pixy","plan","play","plea",
		"pled","plie","plod","plop","plot","plow","ploy","plug","plum","plus","pods","poem","poet","poke","pole","poll","poly","pomp","pond","pong",
		"pony","poof","pool","poop","poor","pope","pops","pore","pork","porn","port","pose","posh","post","pots","pouf","pour","pout","pray","prep",
		"prey","prim","prod","prom","prop","pros","prow","pubs","puce","puck","puff","pugs","puke","pull","pulp","puma","pump","punk","puns","punt",
		"puny","pupa","pups","pure","purr","push","puss","puts","putt","pyre","quad","quay","quid","quip","quit","quiz","race","rack","racy","rads",
		"raft","rage","rags","raid","rail","rain","rake","ramp","rams","rang","rank","rant","rape","raps","rare","rash","rasp","rate","rath","rats",
		"rave","rays","raze","read","real","ream","reap","rear","reck","redo","reds","reed","reef","reek","reel","refs","rein","rely","rend","rent",
		"reps","rest","revs","ribs","rice","rich","rick","ride","rids","rife","riff","rift","rigs","rile","rill","rims","rind","ring","rink","riot",
		"ripe","rips","rise","risk","rite","road","roam","roan","roar","robe","robs","rock","rode","rods","roes","role","roll","romp","roof","rook",
		"room","root","rope","rose","rosy","rote","rots","roux","rove","rows","rubs","ruby","rude","rued","rues","rugs","ruin","rule","ruly","rump",
		"rums","rune","rung","runs","runt","ruse","rush","rust","ruts","ryes","sack","sacs","safe","saga","sage","sags","said","sail","sake","sale",
		"salt","same","sand","sane","sang","sank","sans","saps","sash","sass","sate","save","saws","says","scab","scad","scam","scan","scar","scat",
		"scry","scud","scum","seal","seam","sear","seas","seat","sect","seed","seek","seem","seen","seep","seer","sees","sego","self","sell","semi",
		"send","sent","serf","sets","sewn","sews","sexy","shag","sham","shed","shes","shim","shin","ship","shod","shoe","shoo","shop","shot","show",
		"shun","shut","sick","side","sift","sigh","sign","silk","sill","silo","silt","sine","sing","sink","sins","sips","sire","sirs","site","sits",
		"size","skew","skid","skim","skin","skip","skis","skit","slab","slag","slam","slap","slat","slaw","slay","sled","slew","slid","slim","slip",
		"slit","slob","slog","slop","slot","slow","slug","slum","slur","smog","smug","smut","snag","snap","snip","snob","snog","snot","snow","snub",
		"snug","soak","soap","soar","sobs","sock","soda","sods","sofa","soft","soil","sold","sole","solo","some","song","sons","soon","soot","sops",
		"sore","sort","sots","soul","soup","sour","sown","sows","soys","spam","span","spar","spas","spat","spaz","spec","sped","spew","spin","spit",
		"spot","spry","spud","spun","spur","stab","stag","star","stat","stay","stem","step","stew","stir","stop","stow","stub","stud","stun","stye",
		"subs","such","suck","suds","sued","suer","sues","suit","sulk","sumo","sums","sung","sunk","suns","sups","sure","surf","suss","swab","swag",
		"swam","swan","swap","swat","sway","swig","swim","swob","swum","sync","tabs","tack","taco","tact","tags","tail","take","talc","tale","talk",
		"tall","tame","tamp","tang","tank","tans","tape","taps","tare","tarp","tars","tart","tase","task","taut","taxi","teal","team","tear","teas",
		"teat","teem","teen","tees","tell","temp","tend","tens","tent","term","tern","test","text","than","that","thaw","thee","them","then","they",
		"thin","this","thou","thru","thud","thug","thus","tick","tics","tide","tidy","tied","tier","ties","tiff","tiki","tile","till","tilt","time",
		"tine","tins","tint","tiny","tips","tire","toad","toed","toes","tofu","toga","toil","told","toll","tomb","tome","toms","tone","tons","took",
		"tool","toon","toot","tops","tore","torn","torr","tort","toss","tote","tots","tour","tout","town","tows","toys","tram","trap","tray","tree",
		"trek","trim","trio","trip","trod","trot","true","tuba","tube","tubs","tuck","tuft","tugs","tuna","tune","turf","turn","tush","tusk","tutu",
		"twig","twin","twit","twos","tyke","type","typo","tzar","ugly","umps","undo","unit","unto","updo","upon","urea","urge","urns","ursa","used",
		"user","uses","vain","vale","vamp","vane","vans","vary","vase","vast","vats","veal","veer","vees","veil","vein","veld","vend","vent","verb",
		"very","vest","veto","vets","vial","vibe","vice","vied","vies","view","vile","vine","visa","void","volt","vote","vows","wade","wads","waft",
		"wage","wags","wail","wait","wake","walk","wall","wand","wane","want","ward","ware","warm","warn","warp","wars","wart","wary","wash","wasp",
		"watt","wave","wavy","waxy","ways","weak","wean","wear","webs","weds","weed","week","weep","weld","well","welt","wend","went","wept","were",
		"wert","west","wets","wham","what","whee","when","whet","whew","whey","whig","whim","whip","whir","whiz","whoa","whom","whys","wick","wide",
		"wife","wigs","wild","will","wilt","wily","wimp","wind","wine","wing","wink","wins","wipe","wire","wiry","wise","wish","wisp","with","wits",
		"woes","woke","woks","wolf","womb","wont","wood","woof","wool","woos","word","wore","work","worm","worn","wove","wows","wrap","wren","writ",
		"wuss","yack","yaks","yams","yank","yaps","yard","yarn","yawn","yawp","yaws","yeah","year","yeas","yell","yelp","yeti","yews","yips","yoga",
		"yoke","yolk","yore","your","yuck","yule","yurt","zags","zany","zaps","zeal","zees","zero","zest","zinc","zing","zips","ziti","zits","zone",
		"zonk","zoos","zoom"};
}
